[Godot] Resourceインスタンスの使いまわしに要注意(Local to Sceneの使い方)

まず結論

Godotで使われるResourceは、Local to Sceneを適切に設定しないと、Materialなどのインスタンスは使い回されるので注意。

スクリーンショット 2024-10-14 21.41.56.png

概要

Godotを使っていると自然に気づくことなのだけれど、Materialなど、各ノードのパラメータ等で指定するResourceは、何もしなければ別のノードでも使い回される。

スクリーンショット 2024-10-14 21.47.15.png

↑Resourceの見分け方は、「> Resource」がついているのが特徴。

例えば、ある作成済みのノードをコピーして、元のノードのマテリアルを編集すると、新しいノードでもその値が使われる。つまり、いわゆるシャローコピー (Shallow Copy) の状態になっている。

これを避ける簡単な方法として、「ユニーク化」がある。

スクリーンショット 2024-10-14 21.50.25.png

ユニーク化すれば、別のリソースとなるので、別のノードで別のパラメータなどを指定しても、リンクすることはない。

これで万事解決か?と思っていると、実は同じノードをPrefab的に、何度もリスポーンさせると、リソースの使い回しが発生する

検証用コード

検証のために、以下のような簡単なプロジェクトを作ってみる。(執筆時点でGodot 4.3にて検証。)

スクリーンショット 2024-10-14 21.40.41.png

まずメインシーンとなる test_scene.tscn は以下の通り。

スクリーンショット 2024-10-14 21.40.10.png

ここで TestSceneController (test_scene_controller.gd) は以下の通り。

class_name TestSceneController
extends Node

@export var vw: int = 640
@export var vh: int = 480

@onready var scene_root: Node2D = self.get_parent()

var is_first_time = true

# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	pass # Replace with function body.


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
	pass

func _spawn_obj() -> void:
	var res: Resource = load("res://test_node.tscn")
	var obj: Sprite2D = res.instantiate()
	obj.position = Vector2(randf_range(0, vw), randf_range(0, vh))

	if is_first_time:
		var mat: CanvasItemMaterial = obj.material as CanvasItemMaterial
		mat.blend_mode = CanvasItemMaterial.BLEND_MODE_ADD
		is_first_time = false

	scene_root.add_child(obj)

func _unhandled_input(event: InputEvent) -> void:
	if event is InputEventKey:
		if event.is_pressed():
			if event.keycode == KEY_A:
				_spawn_obj()

このコードは、Aキーが押されるたびにランダムな位置にTestNodeを作る。ただし、初回だけマテリアルのブレンディングをADDにする。

ちなみにTestNode (test_node.gd) は、以下のようなシンプルなSprite2Dで、CanvasItemMaterialがマテリアルに設定してある。

スクリーンショット 2024-10-14 21.40.28.png

スクリーンショット 2024-10-14 21.40.24.png

スクリーンショット 2024-10-14 22.01.13.png

検証用コードの実行

さて、このコードを素朴に実行すると、次のようになる。

スクリーンショット 2024-10-14 22.02.07.png

初回だけにブレンディングをADDに変更したにもかかわらず、リソースが使い回された結果、すべてに適用されてしまっている。

しかも、これらのリソースはコピーされたわけではなく同じインスタンスを指しているので、一つの値を変えると残り全ても変わってしまう。

Local to Sceneの有効化

では次に、Local to Sceneをオンにした上でもう一度実行する。

スクリーンショット 2024-10-14 22.02.18.png

スクリーンショット 2024-10-14 22.02.40.png

今度は、ちゃんと初回だけがADDになって、2回目以降は通常のブレンディングになっている。

ちなみに Local to Scene が何を意味するかはドキュメントに書いてあり、今回の目的通り、各インスタンスが同じものを指すのではなくちゃんとコピーしたい場合にtrueにするとある。

スクリーンショット 2024-10-14 21.41.56.png

(これを再帰的にやってくれるオプションがあれば、さらに子のリソースもディープコピー (Deep Copy) されて便利な気もするけど、そういうオプションってあるのだろうか…?)

まとめ

今回はマテリアルを例として取り上げたものの、すべてのResourceについて同じことがいえるので、例えば動画・音声プレイヤーなどで再生対象のファイルを動的に変えたりするようなシチュエーションでは、注意が必要。

特に、ゲームの2回目の実行時は、一度使われたものを再度リスポーンすることが多いので、これにハマってしまうケースがあるので要注意。

他の言語のシャローコピーやディープコピーと同じく、どちらが良いかはケースバイケースなので、例えば今回のようにコピーするのではなく、ready()のときに必要なパラメータをリセットするコードを呼ぶ、などとの使い分けが必要。